Make {{agent}} universally accessible from any agent.

To implement this, EventDrop is introduced to represent an Event object
in interpolation.

Akinori MUSHA 10 years ago
parent
commit
c1f114d56b

+ 9 - 9
app/concerns/liquid_interpolatable.rb

@@ -1,28 +1,28 @@
1 1
 module LiquidInterpolatable
2 2
   extend ActiveSupport::Concern
3 3
 
4
-  def interpolate_options(options, payload = {})
4
+  def interpolate_options(options, event = {})
5 5
     case options
6 6
       when String
7
-        interpolate_string(options, payload)
7
+        interpolate_string(options, event)
8 8
       when ActiveSupport::HashWithIndifferentAccess, Hash
9
-        options.inject(ActiveSupport::HashWithIndifferentAccess.new) { |memo, (key, value)| memo[key] = interpolate_options(value, payload); memo }
9
+        options.inject(ActiveSupport::HashWithIndifferentAccess.new) { |memo, (key, value)| memo[key] = interpolate_options(value, event); memo }
10 10
       when Array
11
-        options.map { |value| interpolate_options(value, payload) }
11
+        options.map { |value| interpolate_options(value, event) }
12 12
       else
13 13
         options
14 14
     end
15 15
   end
16 16
 
17
-  def interpolated(payload = {})
18
-    key = [options, payload]
17
+  def interpolated(event = {})
18
+    key = [options, event]
19 19
     @interpolated_cache ||= {}
20
-    @interpolated_cache[key] ||= interpolate_options(options, payload)
20
+    @interpolated_cache[key] ||= interpolate_options(options, event)
21 21
     @interpolated_cache[key]
22 22
   end
23 23
 
24
-  def interpolate_string(string, payload)
25
-    Liquid::Template.parse(string).render!(payload, registers: {agent: self})
24
+  def interpolate_string(string, event)
25
+    Liquid::Template.parse(string).render!(event.to_liquid, registers: {agent: self})
26 26
   end
27 27
 
28 28
   require 'uri'

+ 7 - 0
app/models/agent.rb

@@ -402,5 +402,12 @@ class AgentDrop < Liquid::Drop
402 402
     def to_liquid
403 403
       AgentDrop.new(self)
404 404
     end
405
+
406
+    def to_h
407
+      drop = to_liquid
408
+      AgentDrop.public_instance_methods(false).each_with_object({}) { |attr, hash|
409
+        hash[attr.to_s] = drop.__send__(attr)
410
+      }
411
+    end
405 412
   end
406 413
 end

+ 1 - 1
app/models/agents/data_output_agent.rb

@@ -83,7 +83,7 @@ module Agents
83 83
     def receive_web_request(params, method, format)
84 84
       if interpolated['secrets'].include?(params['secret'])
85 85
         items = received_events.order('id desc').limit(events_to_show).map do |event|
86
-          interpolated = interpolate_options(options['template']['item'], event.payload)
86
+          interpolated = interpolate_options(options['template']['item'], event)
87 87
           interpolated['guid'] = event.id
88 88
           interpolated['pubDate'] = event.created_at.rfc2822.to_s
89 89
           interpolated

+ 1 - 1
app/models/agents/email_agent.rb

@@ -29,7 +29,7 @@ module Agents
29 29
       incoming_events.each do |event|
30 30
         log "Sending digest mail to #{user.email} with event #{event.id}"
31 31
         recipients(event.payload).each do |recipient|
32
-          SystemMailer.delay.send_message(:to => recipient, :subject => interpolated(event.payload)['subject'], :headline => interpolated(event.payload)['headline'], :groups => [present(event.payload)])
32
+          SystemMailer.delay.send_message(:to => recipient, :subject => interpolated(event)['subject'], :headline => interpolated(event)['headline'], :groups => [present(event.payload)])
33 33
         end
34 34
       end
35 35
     end

+ 2 - 3
app/models/agents/event_formatting_agent.rb

@@ -106,9 +106,8 @@ module Agents
106 106
 
107 107
     def receive(incoming_events)
108 108
       incoming_events.each do |event|
109
-        agent = Agent.find(event.agent_id)
110
-        payload = perform_matching({ 'agent' => agent }.update(event.payload))
111
-        opts = interpolated(payload)
109
+        payload = perform_matching({ 'agent' => event.agent.to_h }.merge(event.payload))
110
+        opts = interpolated(EventDrop.new(event, payload))
112 111
         formatted_event = opts['mode'].to_s == "merge" ? event.payload.dup : {}
113 112
         formatted_event.merge! opts['instructions']
114 113
         formatted_event['created_at'] = event.created_at unless opts['skip_created_at'].to_s == "true"

+ 2 - 2
app/models/agents/growl_agent.rb

@@ -51,7 +51,7 @@ module Agents
51 51
         message = (event.payload['message'] || event.payload['text']).to_s
52 52
         subject = event.payload['subject'].to_s
53 53
         if message.present? && subject.present?
54
-          log "Sending Growl notification '#{subject}': '#{message}' to #{interpolated(event.payload)['growl_server']} with event #{event.id}"
54
+          log "Sending Growl notification '#{subject}': '#{message}' to #{interpolated(event)['growl_server']} with event #{event.id}"
55 55
           notify_growl(subject,message)
56 56
         else
57 57
           log "Event #{event.id} not sent, message and subject expected"
@@ -59,4 +59,4 @@ module Agents
59 59
       end
60 60
     end
61 61
   end
62
-end
62
+end

+ 1 - 1
app/models/agents/hipchat_agent.rb

@@ -42,7 +42,7 @@ module Agents
42 42
     def receive(incoming_events)
43 43
       client = HipChat::Client.new(interpolated[:auth_token])
44 44
       incoming_events.each do |event|
45
-        mo = interpolated(event.payload)
45
+        mo = interpolated(event)
46 46
         client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color])
47 47
       end
48 48
     end

+ 1 - 1
app/models/agents/jabber_agent.rb

@@ -60,7 +60,7 @@ module Agents
60 60
     end
61 61
 
62 62
     def body(event)
63
-      interpolated(event.payload)['message']
63
+      interpolated(event)['message']
64 64
     end
65 65
   end
66 66
 end

+ 2 - 2
app/models/agents/mqtt_agent.rb

@@ -106,7 +106,7 @@ module Agents
106 106
     def receive(incoming_events)
107 107
       mqtt_client.connect do |c|
108 108
         incoming_events.each do |event|
109
-          c.publish(interpolated(event.payload)['topic'], event.payload)
109
+          c.publish(interpolated(event)['topic'], event)
110 110
         end
111 111
 
112 112
         c.disconnect
@@ -136,4 +136,4 @@ module Agents
136 136
     end
137 137
 
138 138
   end
139
-end
139
+end

+ 2 - 2
app/models/agents/peak_detector_agent.rb

@@ -67,7 +67,7 @@ module Agents
67 67
         if newest_value > average_value + std_multiple * standard_deviation
68 68
           memory['peaks'][group] << newest_time
69 69
           memory['peaks'][group].reject! { |p| p <= newest_time - window_duration }
70
-          create_event :payload => { 'message' => interpolated(event.payload)['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s }
70
+          create_event :payload => { 'message' => interpolated(event)['message'], 'peak' => newest_value, 'peak_time' => newest_time, 'grouped_by' => group.to_s }
71 71
         end
72 72
       end
73 73
     end
@@ -127,4 +127,4 @@ module Agents
127 127
       memory['data'][group].reject! { |value, time| time <= newest_time - window_duration }
128 128
     end
129 129
   end
130
-end
130
+end

+ 2 - 2
app/models/agents/post_agent.rb

@@ -68,7 +68,7 @@ module Agents
68 68
 
69 69
     def receive(incoming_events)
70 70
       incoming_events.each do |event|
71
-        outgoing = interpolated(event.payload)['payload'].presence || {}
71
+        outgoing = interpolated(event)['payload'].presence || {}
72 72
         if interpolated['no_merge'].to_s == 'true'
73 73
           handle outgoing, event.payload
74 74
         else
@@ -125,4 +125,4 @@ module Agents
125 125
       Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == "https") { |http| http.request(req) }
126 126
     end
127 127
   end
128
-end
128
+end

+ 1 - 1
app/models/agents/pushbullet_agent.rb

@@ -49,7 +49,7 @@ module Agents
49 49
     private
50 50
 
51 51
     def query_options(event)
52
-      mo = interpolated(event.payload)
52
+      mo = interpolated(event)
53 53
       {
54 54
         :basic_auth => {:username => mo[:api_key], :password => ''},
55 55
         :body => {:device_iden => mo[:device_id], :title => mo[:title], :body => mo[:body], :type => 'note'}

+ 1 - 1
app/models/agents/pushover_agent.rb

@@ -58,7 +58,7 @@ module Agents
58 58
 
59 59
     def receive(incoming_events)
60 60
       incoming_events.each do |event|
61
-        payload_interpolated = interpolated(event.payload)
61
+        payload_interpolated = interpolated(event)
62 62
         message = (event.payload['message'].presence || event.payload['text'].presence || payload_interpolated['message']).to_s
63 63
         if message.present?
64 64
           post_params = {

+ 2 - 2
app/models/agents/shell_command_agent.rb

@@ -61,7 +61,7 @@ module Agents
61 61
 
62 62
     def receive(incoming_events)
63 63
       incoming_events.each do |event|
64
-        handle(interpolated(event.payload), event)
64
+        handle(interpolated(event), event)
65 65
       end
66 66
     end
67 67
 
@@ -109,4 +109,4 @@ module Agents
109 109
       [result, errors, exit_status]
110 110
     end
111 111
   end
112
-end
112
+end

+ 1 - 1
app/models/agents/slack_agent.rb

@@ -57,7 +57,7 @@ module Agents
57 57
 
58 58
     def receive(incoming_events)
59 59
       incoming_events.each do |event|
60
-        opts = interpolated(event.payload)
60
+        opts = interpolated(event)
61 61
         slack_notifier.ping opts[:message], channel: opts[:channel], username: opts[:username]
62 62
       end
63 63
     end

+ 1 - 1
app/models/agents/translation_agent.rb

@@ -66,7 +66,7 @@ module Agents
66 66
       access_token = JSON.parse(response.body)["access_token"]
67 67
       incoming_events.each do |event|
68 68
         translated_event = {}
69
-        opts = interpolated(event.payload)
69
+        opts = interpolated(event)
70 70
         opts['content'].each_pair do |key, value|
71 71
           translated_event[key] = translate(value.first, opts['to'], access_token)
72 72
         end

+ 2 - 2
app/models/agents/trigger_agent.rb

@@ -57,7 +57,7 @@ module Agents
57 57
     def receive(incoming_events)
58 58
       incoming_events.each do |event|
59 59
 
60
-        opts = interpolated(event.payload)
60
+        opts = interpolated(event)
61 61
 
62 62
         match = opts['rules'].all? do |rule|
63 63
           value_at_path = Utils.value_at(event['payload'], rule['path'])
@@ -105,4 +105,4 @@ module Agents
105 105
       interpolated['keep_event'] == 'true'
106 106
     end
107 107
   end
108
-end
108
+end

+ 3 - 3
app/models/agents/twilio_agent.rb

@@ -44,13 +44,13 @@ module Agents
44 44
       incoming_events.each do |event|
45 45
         message = (event.payload['message'].presence || event.payload['text'].presence || event.payload['sms'].presence).to_s
46 46
         if message.present?
47
-          if interpolated(event.payload)['receive_call'].to_s == 'true'
47
+          if interpolated(event)['receive_call'].to_s == 'true'
48 48
             secret = SecureRandom.hex 3
49 49
             memory['pending_calls'][secret] = message
50 50
             make_call secret
51 51
           end
52 52
 
53
-          if interpolated(event.payload)['receive_text'].to_s == 'true'
53
+          if interpolated(event)['receive_text'].to_s == 'true'
54 54
             message = message.slice 0..160
55 55
             send_message message
56 56
           end
@@ -86,4 +86,4 @@ module Agents
86 86
       end
87 87
     end
88 88
   end
89
-end
89
+end

+ 2 - 2
app/models/agents/twitter_publish_agent.rb

@@ -41,7 +41,7 @@ module Agents
41 41
         incoming_events = incoming_events.first(20)
42 42
       end
43 43
       incoming_events.each do |event|
44
-        tweet_text = interpolated(event.payload)['message']
44
+        tweet_text = interpolated(event)['message']
45 45
         begin
46 46
           tweet = publish_tweet tweet_text
47 47
           create_event :payload => {
@@ -67,4 +67,4 @@ module Agents
67 67
       twitter.update(text)
68 68
     end
69 69
   end
70
-end
70
+end

+ 2 - 2
app/models/agents/weibo_publish_agent.rb

@@ -47,7 +47,7 @@ module Agents
47 47
         incoming_events = incoming_events.first(20)
48 48
       end
49 49
       incoming_events.each do |event|
50
-        tweet_text = Utils.value_at(event.payload, interpolated(event.payload)['message_path'])
50
+        tweet_text = Utils.value_at(event.payload, interpolated(event)['message_path'])
51 51
         if event.agent.type == "Agents::TwitterUserAgent"
52 52
           tweet_text = unwrap_tco_urls(tweet_text, event.payload)
53 53
         end
@@ -83,4 +83,4 @@ module Agents
83 83
     end
84 84
 
85 85
   end
86
-end
86
+end

+ 32 - 0
app/models/event.rb

@@ -41,3 +41,35 @@ class Event < ActiveRecord::Base
41 41
     Agent.receive!(:only_receivers => propagate_ids) unless propagate_ids.empty?
42 42
   end
43 43
 end
44
+
45
+class EventDrop < Liquid::Drop
46
+  def initialize(event, payload = event.payload)
47
+    @event = event
48
+    @payload = payload
49
+  end
50
+
51
+  def before_method(key)
52
+    if @payload.key?(key)
53
+      @payload[key]
54
+    else
55
+      case key
56
+      when 'agent'
57
+        @event.agent
58
+      end
59
+    end
60
+  end
61
+
62
+  # Allow iteration using a "for" loop.  Including Enumerable will
63
+  # enable methods like max, min and sort, but it does not make much
64
+  # sense since this is a hash-like object.
65
+  def each(&block)
66
+    return to_enum(__method__) unless block
67
+    @payload.each(&block)
68
+  end
69
+
70
+  class ::Event
71
+    def to_liquid
72
+      EventDrop.new(self)
73
+    end
74
+  end
75
+end

+ 9 - 2
spec/models/agents/event_formatting_agent_spec.rb

@@ -8,7 +8,8 @@ describe Agents::EventFormattingAgent do
8 8
             :instructions => {
9 9
                 :message => "Received {{content.text}} from {{content.name}} .",
10 10
                 :subject => "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}",
11
-                :agent => "{{agent.type}}",
11
+                :agent_type => "{{agent.type}}",
12
+                :agent_type_via_matching => "{{agent_info.type}}",
12 13
             },
13 14
             :mode => "clean",
14 15
             :matchers => [
@@ -17,6 +18,11 @@ describe Agents::EventFormattingAgent do
17 18
                     :regexp => "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)",
18 19
                     :to => "pretty_date",
19 20
                 },
21
+                {
22
+                    :path => "{{agent.type}}",
23
+                    :regexp => "\\A(?<type>.*)\\z",
24
+                    :to => "agent_info",
25
+                },
20 26
             ],
21 27
             :skip_created_at => "false"
22 28
         }
@@ -64,12 +70,13 @@ describe Agents::EventFormattingAgent do
64 70
     it "should handle Liquid templating in instructions" do
65 71
       @checker.receive([@event])
66 72
       Event.last.payload[:message].should == "Received Some Lorem Ipsum from somevalue ."
67
-      Event.last.payload[:agent].should == "WeatherAgent"
73
+      Event.last.payload[:agent_type].should == "WeatherAgent"
68 74
     end
69 75
 
70 76
     it "should handle matchers and Liquid templating in instructions" do
71 77
       @checker.receive([@event])
72 78
       Event.last.payload[:subject].should == "Weather looks like someothervalue according to the forecast at 10:00 PM EST"
79
+      Event.last.payload[:agent_type_via_matching].should == "WeatherAgent"
73 80
     end
74 81
 
75 82
     it "should allow escaping" do

+ 36 - 0
spec/models/event_spec.rb

@@ -76,3 +76,39 @@ describe Event do
76 76
     end
77 77
   end
78 78
 end
79
+
80
+describe EventDrop do
81
+  def interpolate(string, event)
82
+    event.agent.interpolate_string(string, event.to_liquid)
83
+  end
84
+
85
+  before do
86
+    @event = Event.new
87
+    @event.agent = agents(:jane_weather_agent)
88
+    @event.payload = {
89
+      'title' => 'some title',
90
+      'url' => 'http://some.site.example.org/',
91
+    }
92
+    @event.save!
93
+  end
94
+
95
+  it 'should be created via Agent#to_liquid' do
96
+    @event.to_liquid.class.should be(EventDrop)
97
+  end
98
+
99
+  it 'should have attributes of its payload' do
100
+    t = '{{title}}: {{url}}'
101
+    interpolate(t, @event).should eq('some title: http://some.site.example.org/')
102
+  end
103
+
104
+  it 'should be iteratable' do
105
+    # to_liquid returns self
106
+    t = "{% for pair in to_liquid %}{{pair | join:':' }}\n{% endfor %}"
107
+    interpolate(t, @event).should eq("title:some title\nurl:http://some.site.example.org/\n")
108
+  end
109
+
110
+  it 'should have agent' do
111
+    t = '{{agent.name}}'
112
+    interpolate(t, @event).should eq('SF Weather')
113
+  end
114
+end

+ 5 - 5
spec/support/shared_examples/liquid_interpolatable.rb

@@ -20,7 +20,7 @@ shared_examples_for LiquidInterpolatable do
20 20
 
21 21
   describe "interpolating liquid templates" do
22 22
     it "should work" do
23
-      @checker.interpolate_options(@checker.options, @event.payload).should == {
23
+      @checker.interpolate_options(@checker.options, @event).should == {
24 24
           "normal" => "just some normal text",
25 25
           "variable" => "hello",
26 26
           "text" => "Some test with an embedded hello",
@@ -30,7 +30,7 @@ shared_examples_for LiquidInterpolatable do
30 30
 
31 31
     it "should work with arrays", focus: true do
32 32
       @checker.options = {"value" => ["{{variable}}", "Much array", "Hey, {{hello_world}}"]}
33
-      @checker.interpolate_options(@checker.options, @event.payload).should == {
33
+      @checker.interpolate_options(@checker.options, @event).should == {
34 34
         "value" => ["hello", "Much array", "Hey, Hello world"]
35 35
       }
36 36
     end
@@ -38,7 +38,7 @@ shared_examples_for LiquidInterpolatable do
38 38
     it "should work recursively" do
39 39
       @checker.options['hash'] = {'recursive' => "{{variable}}"}
40 40
       @checker.options['indifferent_hash'] = ActiveSupport::HashWithIndifferentAccess.new({'recursive' => "{{variable}}"})
41
-      @checker.interpolate_options(@checker.options, @event.payload).should == {
41
+      @checker.interpolate_options(@checker.options, @event).should == {
42 42
           "normal" => "just some normal text",
43 43
           "variable" => "hello",
44 44
           "text" => "Some test with an embedded hello",
@@ -49,8 +49,8 @@ shared_examples_for LiquidInterpolatable do
49 49
     end
50 50
 
51 51
     it "should work for strings" do
52
-      @checker.interpolate_string("{{variable}}", @event.payload).should == "hello"
53
-      @checker.interpolate_string("{{variable}} you", @event.payload).should == "hello you"
52
+      @checker.interpolate_string("{{variable}}", @event).should == "hello"
53
+      @checker.interpolate_string("{{variable}} you", @event).should == "hello you"
54 54
     end
55 55
   end
56 56